/*  
 * Copyright 2012 Sebastian Annies, Hamburg
 *
 * Licensed under the Apache License, Version 2.0 (the License); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an AS IS BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */

package com.googlecode.mp4parser;

import com.coremedia.iso.BoxParser;
import com.coremedia.iso.ChannelHelper;
import com.coremedia.iso.Hex;
import com.coremedia.iso.IsoFile;
import com.coremedia.iso.IsoTypeWriter;
import com.coremedia.iso.boxes.Box;
import com.coremedia.iso.boxes.ContainerBox;
import com.coremedia.iso.boxes.UserBox;
import com.googlecode.mp4parser.annotations.DoNotParseDetail;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.logging.Logger;

import static com.googlecode.mp4parser.util.CastUtils.l2i;

/**
 * A basic on-demand parsing box. Requires the implementation of three methods to become a fully
 * working box: <ol> <li>{@link #_parseDetails(java.nio.ByteBuffer)}</li> <li>{@link
 * #getContent(java.nio.ByteBuffer)}</li> <li>{@link #getContentSize()}</li> </ol> additionally this
 * new box has to be put into the <code>isoparser-default.properties</code> file so that it is
 * accessible by the <code>PropertyBoxParserImpl</code>
 */
public abstract class AbstractBox implements Box {
  public static int MEM_MAP_THRESHOLD = 100 * 1024;
  private static Logger LOG = Logger.getLogger(AbstractBox.class.getName());

  protected String type;
  private byte[] userType;
  private ContainerBox parent;

  private ByteBuffer content;
  private ByteBuffer deadBytes = null;


  protected AbstractBox(String type) {
    this.type = type;
  }

  protected AbstractBox(String type, byte[] userType) {
    this.type = type;
    this.userType = userType;
  }

  /**
   * Get the box's content size without its header. This must be the exact number of bytes that
   * <code>getContent(ByteBuffer)</code> writes.
   *
   * @return Gets the box's content size in bytes
   * @see #getContent(java.nio.ByteBuffer)
   */
  protected abstract long getContentSize();

  /**
   * Write the box's content into the given <code>ByteBuffer</code>. This must include flags and
   * version in case of a full box. <code>byteBuffer</code> has been initialized with
   * <code>getSize()</code> bytes.
   *
   * @param byteBuffer the sink for the box's content
   */
  protected abstract void getContent(ByteBuffer byteBuffer);

  /**
   * Parse the box's fields and child boxes if any.
   *
   * @param content the box's raw content beginning after the 4-cc field.
   */
  protected abstract void _parseDetails(ByteBuffer content);

  /**
   * Read the box's content from a byte channel without parsing it. Parsing is done on-demand.
   *
   * @param readableByteChannel the (part of the) iso file to parse
   * @param contentSize         expected contentSize of the box
   * @param boxParser           creates inner boxes
   * @throws IOException in case of an I/O error.
   */
  @DoNotParseDetail
  public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException {
    if (readableByteChannel instanceof FileChannel && contentSize > MEM_MAP_THRESHOLD) {
      // todo: if I map this here delayed I could use transferFrom/transferTo in the getBox method
      // todo: potentially this could speed up writing.
      //
      // It's quite expensive to map a file into the memory. Just do it when the box is larger than a MB.
      content = ((FileChannel) readableByteChannel).map(FileChannel.MapMode.READ_ONLY, ((FileChannel) readableByteChannel).position(), contentSize);
      ((FileChannel) readableByteChannel).position(((FileChannel) readableByteChannel).position() + contentSize);
    } else {
      assert contentSize < Integer.MAX_VALUE;
      content = ChannelHelper.readFully(readableByteChannel, contentSize);
    }
    if (isParsed() == false) {
      parseDetails();
    }

  }

  public void getBox(WritableByteChannel os) throws IOException {
    ByteBuffer bb = ByteBuffer.allocate(l2i(getSize()));
    getHeader(bb);
    if (content == null) {
      getContent(bb);
      if (deadBytes != null) {
        deadBytes.rewind();
        while (deadBytes.remaining() > 0) {
          bb.put(deadBytes);
        }
      }
    } else {
      content.rewind();
      bb.put(content);
    }
    bb.rewind();
    os.write(bb);
  }


  /**
   * Parses the raw content of the box. It surrounds the actual parsing which is done
   */
  synchronized final void parseDetails() {
    if (content != null) {
      ByteBuffer content = this.content;
      this.content = null;
      content.rewind();
      _parseDetails(content);
      if (content.remaining() > 0) {
        deadBytes = content.slice();
      }
      assert verify(content);
    }
  }

  /**
   * Sets the 'dead' bytes. These bytes are left if the content of the box has been parsed but not
   * all bytes have been used up.
   *
   * @param newDeadBytes the unused bytes with no meaning but required for bytewise reconstruction
   */
  protected void setDeadBytes(ByteBuffer newDeadBytes) {
    deadBytes = newDeadBytes;
  }


  /**
   * Gets the full size of the box including header and content.
   *
   * @return the box's size
   */
  public long getSize() {
    long size = (content == null ? getContentSize() : content.limit());
    size += (8 + // size|type
        (size >= ((1L << 32) - 8) ? 8 : 0) + // 32bit - 8 byte size and type
        (UserBox.TYPE.equals(getType()) ? 16 : 0));
    size += (deadBytes == null ? 0 : deadBytes.limit());
    return size;
  }

  @DoNotParseDetail
  public String getType() {
    return type;
  }

  @DoNotParseDetail
  public byte[] getUserType() {
    return userType;
  }

  @DoNotParseDetail
  public ContainerBox getParent() {
    return parent;
  }

  @DoNotParseDetail
  public void setParent(ContainerBox parent) {
    this.parent = parent;
  }

  @DoNotParseDetail
  public IsoFile getIsoFile() {
    return parent.getIsoFile();
  }

  /**
   * Check if details are parsed.
   *
   * @return <code>true</code> whenever the content <code>ByteBuffer</code> is not <code>null</code>
   */
  public boolean isParsed() {
    return content == null;
  }


  /**
   * Verifies that a box can be reconstructed byte-exact after parsing.
   *
   * @param content the raw content of the box
   * @return <code>true</code> if raw content exactly matches the reconstructed content
   */
  private boolean verify(ByteBuffer content) {
    ByteBuffer bb = ByteBuffer.allocate(l2i(getContentSize() + (deadBytes != null ? deadBytes.limit() : 0)));
    getContent(bb);
    if (deadBytes != null) {
      deadBytes.rewind();
      while (deadBytes.remaining() > 0) {
        bb.put(deadBytes);
      }
    }
    content.rewind();
    bb.rewind();


    if (content.remaining() != bb.remaining()) {
      LOG.severe(this.getType() + ": remaining differs " + content.remaining() + " vs. " + bb.remaining());
      return false;
    }
    int p = content.position();
    for (int i = content.limit() - 1, j = bb.limit() - 1; i >= p; i--, j--) {
      byte v1 = content.get(i);
      byte v2 = bb.get(j);
      if (v1 != v2) {
        LOG.severe(String.format("%s: buffers differ at %d: %2X/%2X", this.getType(), i, v1, v2));
        byte[] b1 = new byte[content.remaining()];
        byte[] b2 = new byte[bb.remaining()];
        content.get(b1);
        bb.get(b2);
        System.err.println("original      : " + Hex.encodeHex(b1, 4));
        System.err.println("reconstructed : " + Hex.encodeHex(b2, 4));
        return false;
      }
    }
    return true;

  }

  private boolean isSmallBox() {
    return (content == null ? (getContentSize() + (deadBytes != null ? deadBytes.limit() : 0) + 8) : content.limit()) < 1L << 32;
  }

  private void getHeader(ByteBuffer byteBuffer) {
    if (isSmallBox()) {
      IsoTypeWriter.writeUInt32(byteBuffer, this.getSize());
      byteBuffer.put(IsoFile.fourCCtoBytes(getType()));
    } else {
      IsoTypeWriter.writeUInt32(byteBuffer, 1);
      byteBuffer.put(IsoFile.fourCCtoBytes(getType()));
      IsoTypeWriter.writeUInt64(byteBuffer, getSize());
    }
    if (UserBox.TYPE.equals(getType())) {
      byteBuffer.put(getUserType());
    }


  }
}
